﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Crocodile.SimplexLanguage.Attributes;
using Crocodile.SimplexLanguage.Tree;
using Fine.Utils.Functions;
using JetBrains.Annotations;



namespace Crocodile.SimplexLanguage.Gardener
{
    using bf = System.Reflection.BindingFlags;


    /// <summary>
    /// Tree maker.
    /// </summary>
    /// <stereotype>service</stereotype>
    public class SimplexGardener
    {

        #region TUNING

        struct EntityClass
        {
            internal readonly Regex Pattern;
            internal readonly Type Clazz;

            public EntityClass(Regex pattern, Type clazz)
            {
                Pattern = pattern;
                Clazz = clazz;
            }
        }


        private readonly List<EntityClass> entityClasses =
            new List<EntityClass>();


        public void SetupEntityClasses(params Type[] classes)
        {
            entityClasses.AddRange
                (
                    classes
                        .SelectMany(c => c.GetCustomAttributes(typeof(SimplexEntityAttribute), false),
                                    (c, o) => new { clazz = c, attr = o as SimplexEntityAttribute })
                        .Select(x => new EntityClass(x.attr.Pattern, x.clazz))
                );
        }


        #endregion


        /// <summary>
        /// Loads the given simplex project into specified root object.
        /// </summary>
        /// <param name="root">root object.</param>
        /// <param name="simplexProject">a simplex project to load.</param>
        public void MakeTree(object root, SimplexProject simplexProject)
        {
            foreach (var simplexFile in simplexProject.Files)
                ProcessFile(root, simplexFile);
        }


        private void ProcessFile(object root, SimplexFile simplexFile)
        {
            ProcessEntityContent(root, simplexFile.Content);
        }


        private void ProcessEntityContent(object obj, IEnumerable<SimplexEntity> Content)
        {
            foreach (var entity in Content)
                ProcessNestedEntity(obj, entity);
        }


        private void ProcessNestedEntity(object obj, SimplexEntity nestedEntity)
        {
            // if this is a property
            Type clazz = obj.GetType();
            foreach (var attro in clazz.GetCustomAttributes(typeof(SimplexPropertiesAttribute), false))
            {
                var attr = attro as SimplexPropertiesAttribute;
                // attr._assert_not_null_();
                Match m = attr.Pattern.Match(nestedEntity.Line);
                if (m.Success)
                {
                    ProcessProperties(obj, attr.Pattern, m, nestedEntity);
                    if (nestedEntity.HasContent)
                        SyntaxError(nestedEntity, "Unexpected inner content.");
                    return;
                }
            }

            // if this is a procedure invocation
            var methods = clazz.GetMethods(bf.InvokeMethod | bf.Public | bf.Instance);
            foreach (var method in methods)
            {
                bool found = false;

                foreach (var attro in method.GetCustomAttributes(typeof(SimplexProcedureAttribute),false))
                {
                    var attr = attro as SimplexProcedureAttribute;
                    Match m = attr.Pattern.Match(nestedEntity.Line);
                    if (m.Success)
                    {
                        ProcessMethodInvocation(obj, method, attr.Pattern, m, nestedEntity);
                        found = true;
                    }
                }

                if (found) return;
            }

            // if it is an auto-property
            foreach (var attro in clazz.GetCustomAttributes(typeof(SimplexAutoPropertiesAttribute),true))
            {
                var attr = attro as SimplexAutoPropertiesAttribute;
                // attr._assert_not_null_();
                Match m = attr.Pattern.Match(nestedEntity.Line);
                if (m.Success)
                {
                    ProcessAutoProperty(obj,attr.Pattern,m,nestedEntity);
                    if (nestedEntity.HasContent)
                        SyntaxError(nestedEntity,"Unexpected inner content.");
                    return;
                }
            }

            // if this is a child (sub-entity)
            foreach (var entityClass in entityClasses)
            {
                Match m = entityClass.Pattern.Match(nestedEntity.Line);
                if (m.Success)
                {
                    var childClass = entityClass.Clazz;
                    var childConstructor = childClass.GetConstructor(noTypes);
                    if (childConstructor == null)
                    {
                        SyntaxError(nestedEntity, "Class {0} has no default constructors.", childClass.Name);
                        return;
                    }

                    var childObject =
                        childConstructor.Invoke(null);
                    // childObject._assert_not_null_();
                    ProcessProperties(childObject, entityClass.Pattern, m, nestedEntity);
                    if (nestedEntity.HasContent)
                        ProcessEntityContent(childObject, nestedEntity.Content);
                    Adopt(obj, childObject, nestedEntity);
                    return;
                }
            }

            // nothing matched
            SyntaxError(nestedEntity, "Unmatched syntax: {0}", nestedEntity.Line);
        }


        private void Adopt([NotNull] object parentObject, [NotNull] object childObject, [NotNull] SimplexEntity childEntity)
        {
            Type parentClass = parentObject.GetType(),
                 childClass = childObject.GetType();

            foreach (var field in parentClass.GetFields())
            {
                foreach (var attro in field.GetCustomAttributes(typeof(SimplexContentAttribute),false))
                {
                    var attr = attro as SimplexContentAttribute;
                    if (attr.Clazz.IsAssignableFrom(childClass))
                    {
                        try
                        {
                            var fieldObject = field.GetValue(parentObject);
                            var fieldObjectType = fieldObject.GetType();
                            fieldObjectType.InvokeMember("Add",
                                BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public,
                                                         null,
                                                         fieldObject,
                                                         new object[] {childObject});
                        }
                        catch (Exception e)
                        {
                            SyntaxError(childEntity, "Class {0} could not adopt class {1} - the method {2}.Add() got exception: {3}", 
                                        parentClass.Name, childClass.Name, field.Name, e.Message);
                        }

                        return;
                    }

                }
            }

            SyntaxError(childEntity, "class {0} could not adopt class {1}", parentClass.Name, childClass.Name);
        }


        private void ProcessProperties(object obj, Regex pattern, Match match, SimplexEntity entity)
        {
            var pairs =
                pattern
                    .GetGroupNames()
                    .Where(NamedGroup)
                    .Select(nm => new {name = nm, value = match.Groups[nm].Value})
                    .Where(x => x.value != null && x.value.Length > 0);

            if (pairs.Count() == 0)
                return;

            foreach (var pair in pairs)
                SetObjectDatum(obj, pair.name, pair.value, entity, pattern);
        }


        private void ProcessAutoProperty(object obj, Regex pattern, Match match, SimplexEntity entity)
        {
            var nameGrp = match.Groups["name"];
            var valueGrp = match.Groups["value"];

            if (nameGrp == null || nameGrp.Length == 0)
            {
                SyntaxError(entity, "Name not recognized.");
                return;
            }
            
            if (valueGrp == null || valueGrp.Length == 0)
            {
                SyntaxError(entity, "Value not recognized.");
                return;
            }

            string name = nameGrp.Value,
                   value = valueGrp.Value;

            SetObjectDatum(obj, name, value, entity, pattern);
        }


        private void ProcessMethodInvocation(object obj, MethodInfo method, Regex pattern, Match match, SimplexEntity nestedEntity)
        {
            var parameters = method.GetParameters();
            int n = parameters.Length;
            var args = new object[n];

            for (int i = 0; i < n; i++)
            {
                string theName = parameters[i].Name;
                int thePatternGroupNr = pattern.GroupNumberFromName(theName);
                if (thePatternGroupNr >= 0 && match.Groups[thePatternGroupNr].Length > 0) // such name exists in the pattern
                {
                    args[i] = CoerceValue(match.Groups[thePatternGroupNr].Value,
                                          parameters[i].ParameterType);
                }
                else
                {
                    if (parameters[i].IsOptional)
                        args[i] = parameters[i].DefaultValue;
                    else
                        SyntaxError(nestedEntity, "Method {0} argument {1} is not specified.", method.Name, theName);
                }
            }

            try
            {
                method.Invoke(obj, args);
            }
            catch (Exception e)
            {
                SyntaxError(nestedEntity,
                            "Class {0} method {1}: failed to invoke the method, exception: {2}",
                            obj.GetType().Name, method.Name, e.Message);
            }
        }


        /// <summary>
        /// Use <c>Fine.Utils.Functions.ObjectExt.ImportDatum()</c> instead.
        /// </summary>
        [Obsolete("Use Fine.Utils.Functions.ObjectExt.ImportDatum() instead.")]
        private void SetObjectDatum(object obj, string name, string value, SimplexEntity entity, object pattern)
        {
            var objClass = obj.GetType();

            try
            {
                var property = objClass.GetProperty(name);
                if (property != null)
                {
                    SetObjectProperty(obj, property, value);
                    return;
                }

                var field = objClass.GetField(name);
                if (field != null)
                {
                    SetObjectField(obj, field, value);
                    return;
                }
            }
            catch (UnsupportedTypeException ute)
            {
                SyntaxError(entity,
                            "Class {0} property {1} is of unsupported type {2}",
                            objClass, name, ute.TypeName);
            }
            catch (Exception e)
            {
                SyntaxError(entity,
                            "Class {0} property {1}: failed to set value \"{2}\", exception: {3}",
                            objClass, name, value, e.Message);
            }

            SyntaxError(entity, "Class {0} has no member {1} for applying rule \"{2}\"", objClass, name, pattern.ToString());
        }


        private static void SetObjectProperty(object obj, PropertyInfo property, string value)
        {
            var coercedValue = CoerceValue(value, property.PropertyType);
            property.SetValue(obj, coercedValue, null);
        }   


        private static void SetObjectField(object obj, FieldInfo field, string value)
        {
            var coercedValue = CoerceValue(value, field.FieldType);
            field.SetValue(obj, coercedValue);
        }


        private static object CoerceValue(string value, Type fieldType)
        {
            string tvalue = value.Trim();

            switch (fieldType.Name)
            {
                case "String":
                case "Object":
                    return value;

                case "Boolean":
                    return tvalue.Matching(BooleanTruePattern);

                case "Byte":
                    return Byte.Parse(tvalue);

                case "SByte":
                    return SByte.Parse(tvalue);

                case "Int16":
                    return Int16.Parse(tvalue);

                case "UInt16":
                    return UInt16.Parse(tvalue);

                case "Int32":
                    return Int32.Parse(tvalue);

                case "UInt32":
                    return UInt32.Parse(tvalue);

                case "Int64":
                    return Int64.Parse(tvalue);

                case "UInt64":
                    return UInt64.Parse(tvalue);

                default:
                    var parsingMethod = fieldType.GetMethod("Parse", bf.Public | bf.Static);
                    if (parsingMethod != null)
                    {
                        try
                        {
                            var valueObj = parsingMethod.Invoke(null, new[] {tvalue});

                            // TODO проверить что получился объект нужного типа
                            return valueObj;
                        }
                        catch (Exception e)
                        {
                            throw new UnsupportedTypeException(fieldType, e);
                        }                        
                    }
                    else
                    {
                        throw new UnsupportedTypeException(fieldType);
                    }
            }
        }


        private static readonly Regex BooleanTruePattern =
                new Regex(@"(t(rue)?|y(es)?|1|\+)");


        private static bool NamedGroup(string groupName)
        {
            char c1 = (groupName ?? "").Length > 0 ? groupName[0] : '\0';
            return Char.IsLetter(c1) || c1 == '_';
        }

        private static readonly Type[] noTypes = new Type[0];

        

        #region ERRORS

        private IList<SimplexSyntaxError> errors = new List<SimplexSyntaxError>();


        [StringFormatMethod("message")]
        private void SyntaxError([NotNull] SimplexEntity entity,
                                 [NotNull] string message,
                                 params object[] args)
        {
            var error = new SimplexSyntaxError(entity,message,args);
            errors.Add(error);
        }


        public SimplexSyntaxError[] getErrors()
        {
            return errors.ToArray();
        }


        public bool HasErrors
        {
            get { return errors.Count > 0; }
        }


        #endregion


        class UnsupportedTypeException : Exception
        {
            internal readonly string TypeName;

            public UnsupportedTypeException(Type type) 
                : base("Unsupported type: " + type.Name)
            {
                TypeName = type.Name;
            }

            public UnsupportedTypeException(Type type, Exception e) 
                : base("Unsupported type: " + type.Name, e)
            {
                TypeName = type.Name;
            }
        }

    }
}
